JWTによるJSONに対する電子署名と、そのユースケース
よく訓練されたアップル信者、都元です。最近、OpenID Connectにどっぷり浸かっております。IAMも好きなんですが、どうもIdentityおじさんの気があるんでしょうか。
さて、OpenID Connectの話は追々ご紹介していきたいと思うのですが。今日はJWTという技術についてご紹介します。
JWT
JWTは JSON Web Token の略で、jot(ジョット)と発音します。まずはイメージを持っていただくために、JWTの例を示します。
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyaG9nZSIsImF1ZCI6ImF1ZGhvZ2UiLCJpc3MiOiJodHRwczpcL1wvZXhhbXBsZS5jb21cLyIsImV4cCI6MTQ1MjU2NTYyOCwiaWF0IjoxNDUyNTY1NTY4fQ.BfW2a1SMY1a8cjb7ATcJPwYSmB1P6l4oN2QRNtod-xCyochsB1ZurxNLqPOGFr7_Abqpk-_lOUaPOdL2jQ23T1DS1lmQgaEZgUXaJPAqGSJygANpv8ds07Q6pbX_XbFpJdoVCQHzP8MjbjW_ft2ZAAJzjZZEC6Hm0WUxKS6V0yRNmEUyV-Ljh6n337rtAoSTks2APdgW1hffyZMrptKKazG2m0V0LQRnu5lmWLTYwTZC8NdNJZNepPwQiGNZS0IcrdZhguGAL75ZGTMw9O_EC9gv_I_9I5NUwZk6LG1feEy3MawT0QaTEsF5n6yUKJ8ziuMXnUEsymGdKC-VYEbPyw
これがJWTです。一見ランダムに見える、長ったらしい文字列ですね。
JWTはJWSまたはJWEのことを表します。何のこっちゃ。という問いは置き去りにします(!?)。ひとまず本稿ではJWEにあまり触れないので、JWTと言われたらJWSのことだと考えてください。
JWS (JSON Web Signature)は、ザックリ言うと「JSONに電子署名をして、URL-safeな文字列として表現したもの」です。参考までに、JWE (JSON Web Encryption)は「JSONを暗号化して、URL-safeな文字列として表現したもの」です。
電子署名や暗号化については数々のアルゴリズムがあると思いますが、本稿ではRSAを利用した非対称鍵暗号を前提に説明していきます。
RSAを使った電子署名は、まずキーペア(秘密鍵+公開鍵)を生成し、秘密鍵は必要最低限の範囲で機密情報として保持します。公開鍵はその名の通り、誰に対して開示しても構いません。
その上で、秘密鍵で署名を行い、公開鍵で検証を行います。署名ができるのは秘密鍵を持っている人だけで、検証は公開鍵の開示を受けた人は誰でも実施可能です。
JWSに含むJSONは暗号化ではなく電子署名であるため、誰でも読み出しが可能です。しかし、電子署名がついているため、内容の改ざんができません。
JWTの構造
さて、先ほど例に挙げたJWTは、JSONに電子署名がしてあるものなわけです。中身はどんなJSONなんでしょうか? それを知るために、JWSの構造を見ていきましょう。
JWSの文字列の中には、ピリオド.
が2つ含まれています。そこを境目にして3つの文字列に分割できます。1つ目はヘッダと呼ばれ、どんなアルゴリズムで署名されているのか等のメタ情報を含みます。2つ目はペイロードと呼ばれ、JSON本体に相当する情報です。3つ目は署名情報が入っています。
中身の情報を見るためには、ペイロードがあればいいわけです。先ほど例に挙げたJWTのペイロードは下記の通りです。
eyJzdWIiOiJ1c2VyaG9nZSIsImF1ZCI6ImF1ZGhvZ2UiLCJpc3MiOiJodHRwczpcL1wvZXhhbXBsZS5jb21cLyIsImV4cCI6MTQ1MjU2NTYyOCwiaWF0IjoxNDUyNTY1NTY4fQ
実はこれ、JSONをBase64urlエンコーディングしたものです。Base64エンコーディングには細かい派生仕様が数多くあります。この辺りはBase64 - Wikipediaによくまとまっています。
ということで、この文字列をデコードしてみます。下記は、Mac OSXに含まれているbase64
コマンドを利用しました。また、末尾にパディング=
を手動で付与しています。(Base64とBase64urlの差分吸収のため)
$ echo eyJzdWIiOiJ1c2VyaG9nZSIsImF1ZCI6ImF1ZGhvZ2UiLCJpc3MiOiJodHRwczpcL1wvZXhhbXBsZS5jb21cLyIsImV4cCI6MTQ1MjU2NTYyOCwiaWF0IjoxNDUyNTY1NTY4fQ== | base64 -d | jq . { "sub": "userhoge", "aud": "audhoge", "iss": "https://example.com/", "exp": 1452565628, "iat": 1452565568 }
参考までに、ヘッダも同じようにデコードできます。
$ echo eyJhbGciOiJSUzI1NiJ9 | base64 -d | jq . { "alg": "RS256" }
署名部分についてもデコードは可能ですが、署名検証にしか利用しないので、コマンドベースでデコードしてもあまり意味はないと思います。
JWTのユースケース
さて色々見てきましたが、JWTは何に使えるのでしょうか。最近OAuth 2.0やOpenID Connectの仕様を追っていて、いくつかのユースケースに触れたのでご紹介します。
OAuth 2.0のアクセストークンとして利用する
OAuth 2.0では、認可サーバがクライアントに対してアクセストークンを発行します。クライアントはアクセストークンを使って、リソースサーバに対する操作をします。
+--------+ +---------------+ | |--(A)- Authorization Request ->| Resource | | | | Owner | | |<-(B)-- Authorization Grant ---| | | | +---------------+ | | | | +---------------+ | |--(C)-- Authorization Grant -->| Authorization | | Client | | Server | | |<-(D)----- Access Token -------| | | | +---------------+ | | | | +---------------+ | |--(E)----- Access Token ------>| Resource | | | | Server | | |<-(F)--- Protected Resource ---| | +--------+ +---------------+ Figure 1: Abstract Protocol Flow
さて、アクセストークンを受け取ったリソースサーバは、受け取ったアクセストークンが真正かどうか(正しい認可サーバが正しく発行したものかどうか)を確認する必要があります。が、この手法についてはOAuth 2.0の仕様のスコープ外とされています。
現実的には下記のようなことを行います。
- 認可サーバとリソースサーバで共有するストレージ(データベース)を持つ。認可サーバはトークン発行時にそのトークンをストレージに書き込み、リソースサーバは受け取ったトークンがストレージにあるかどうかを検証する。
- 認可サーバが「このトークンは有効であるかどうか」の検証を受け付けるAPIエンドポイントを持つ。リソースサーバは受け取ったトークンを認可サーバに渡し、これが有効であるかどうかを検証してもらう。参考: RFC 7662
一方で、このアクセストークンをJWTとして発行したらどうなるでしょうか。ペイロードのJSONとしては最低限「ユーザID」と「有効期限」くらいを持っておきます。そして秘密鍵は認可サーバしか持っておらず、リソースサーバは公開鍵を持っている状態です。
JWTを受け取ったリソースサーバは、公開鍵を使ってその署名を検証できればそのトークンを正しいものだと判断できます。共有のストレージやAPI等は必要なくなります。悪意のあるクライアント等がユーザIDや有効期限を書き換えたとしても、その時は署名検証ができないため、失敗させることができますね。
この方式はいいことずくめな気もしますが、一点工夫が必要です。一度発行してしまったアクセストークンの無効化(revoke)が簡単にはできないのです。この点についてはアクセストークンの有効期限を充分に短くしておくことで要件を満たせれば、それで良いと思います。が、即座の無効化がどうしても必要であれば、やはり無効化されていないかどうか検証するための共有ストレージやAPI等の仕組みが必要です。
OpenID ConnectのIDトークン
OpenID Connect (OIDC) は、しばしばOAuth 2.0と並んで語られることが多い仕組みですが、OIDCが実現したいのはAPIリソースアクセス制御ではありません。
OIDCを理解するにあたっては、ひとまずOAuthのことは忘れてしまったほうが良いです。
まず、普通に何らかのWebシステムを作ることを考えてください。Webシステムには多くの場合、ユーザをID/password等で認証してログインさせるような仕組みが備わっていると思います。しかし、IDリスト及びそれに基づいた認証作業を自分たちのシステム内で行うのではなく、外部のシステムに委譲したいことがあります。具体的には、Facebookログイン等です。
この仕組みは、大雑把に言えば下記のような手順で成り立っています。
- あるシステムfooはFacebookを信頼し、Facebookに対して「この人を認証してください」という依頼を出す。具体的には、ユーザのブラウザをFacebookの特定のURLにリダイレクトする。
- 依頼を受けたFacebookは「この人は bar さんであることを、foo に対して証明する。有効期限はxxxまで。」という文書に電子署名する。この情報を「IDトークン」と呼ぶ。
- Facebookはfooに対して再びリダイレクト(IDトークンを付けて)する。
- JWTを受け取ったfooはFacebookの公開鍵で検証を行い、このJWTは確かにFacebookが発行したものであるという確信を持つ。
- 文書に記述された情報を確認する。例えば有効期限は切れていないか。「foo に対して」の部分に間違いがないか等。
- この時点でfooは、ユーザがbarさんであるとして、システムをログイン状態にして良い。
お察しかと思いますが、OIDCでは「IDトークン」にJWTを利用しています。
ちなみに、JWTに含む情報「foo に対して」の部分は実はOIDCにおいて重要な要素です。この検証を怠ると「トークン置換攻撃」に脆弱なシステムとなるので注意が必要です。
メール着信確認トークン
さて最後に、もうちっと軽いユースケースをご紹介。
Webシステムにおいて、メールアドレスの登録時に確認メールを送り、そのメールに含まれるURLを踏ませる、という手続きがよくあると思います。アレを実装しようとした場合、皆さんはどんな手を使うでしょうか?
素朴に実装しようとするとこんな感じでしょうか。
- ランダム文字列のトークンを発行して、URLにトークンを含ませてメールを送信する。
- それに伴って下記の情報をDBに記録しておく。
- トークン
- ユーザID
- 新しいメールアドレス
- 有効期限
- URLとして受け取ったトークンから上記を取り出し、有効期限等を検証した上で、メールアドレスを更新する。
これ、JWTを使えば…。
- 下記の情報をJWTとして署名し、URLにこのJWTを含ませてメールを送信する。
- ユーザID
- 新しいメールアドレス
- 有効期限
- URLとして受け取ったトークンから上記を取り出し、署名と有効期限等を検証した上で、メールアドレスを更新する。
DBが要らなくなりますね!